package com.liuqianfei.markme;

import com.liuqianfei.markme.common.Utils;
import com.liuqianfei.markme.core.Configuration;
import com.liuqianfei.markme.core.Token;
import com.liuqianfei.markme.render.CodeBlockRender;
import com.liuqianfei.markme.render.CodeInlineRender;
import com.liuqianfei.markme.render.FenceRender;
import com.liuqianfei.markme.render.HardbreakRender;
import com.liuqianfei.markme.render.HtmlBlockRender;
import com.liuqianfei.markme.render.HtmlInlineRender;
import com.liuqianfei.markme.render.ImageRender;
import com.liuqianfei.markme.render.SoftbreakRender;
import com.liuqianfei.markme.render.TextRender;
import com.liuqianfei.markme.rules.Environment;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 渲染器
 * @author liuqianfei
 */
public class Renderer
{
    Map<String, IRender> rules;

    /**
     * new Renderer()
     *
     * Creates new [[Renderer]] instance and fill [[Renderer#rules]] with defaults.
     **/
    public Renderer()
    {
        /**
         * Renderer#rules -> Object
         *
         * Contains render rules for tokens. Can be updated and extended.
         *
         * ##### Example
         *
         * ```javascript
         * var md = require('markdown-it')();
         *
         * md.renderer.rules.strong_open  = function () { return '<b>'; };
         * md.renderer.rules.strong_close = function () { return '</b>'; };
         *
         * var result = md.renderInline(...);
         * ```
         *
         * Each rule is called as independed static function with fixed signature:
         *
         * ```javascript
         * function my_token_render(tokens, idx, options, env, renderer) {
         *   // ...
         *   return renderedHTML;
         * }
         * ```
         *
         * See [source code](https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.js)
         * for more details and examples.
         **/
        //this.rules = assign({}, default_rules);

        this.rules = new HashMap<>();

        this.rules.put("code_inline", new CodeInlineRender());
        this.rules.put("code_block", new CodeBlockRender());
        this.rules.put("fence", new FenceRender());
        this.rules.put("image", new ImageRender());
        this.rules.put("hardbreak", new HardbreakRender());
        this.rules.put("softbreak", new SoftbreakRender());
        this.rules.put("text", new TextRender());
        this.rules.put("html_block", new HtmlBlockRender());
        this.rules.put("html_inline", new HtmlInlineRender());
    }

    public Renderer(Map<String, IRender> rules)
    {
        this.rules.putAll(rules);
    }
    
    /**
     * Render token attributes to string.
     **/
    public String renderAttrs(Token token)
    {
        StringBuilder result = new StringBuilder();

        if (null == token.attrs || token.attrs.size() == 0)
            return "";

        for (Map.Entry<String, String> attr : token.attrs.entrySet())
        {
            result.append(" ")
                .append(Utils.escapeHtml(attr.getKey())).append("=\"")
                .append(Utils.escapeHtml(attr.getValue())).append("\"");
        }
      
        return result.toString();
    }
    
    /**
     * Renderer.renderToken(tokens, idx, options) -> String
     * - tokens (Array): list of tokens
     * - idx (Numbed): token index to render
     * - options (Object): params of parser instance
     *
     * Default token renderer. Can be overriden by custom function
     * in [[Renderer#rules]].
     **/
    public String renderToken(List<Token> tokens, int idx, Configuration configuration)
    {
        StringBuilder result = new StringBuilder();
        Token nextToken;
        boolean needLf = false;
        Token token = tokens.get(idx);

        // Tight list paragraphs
        if (token.hidden)
            return "";

        // Insert a newline between hidden paragraph and subsequent opening
        // block-level tag.
        //
        // For example, here we should insert a newline before blockquote:
        // - a
        // >
        //
        if (token.block && token.nesting != -1 && idx > 0 && tokens.get(idx - 1).hidden)
            result.append("\n");

        // Add token name, e.g. `<img`
        result.append(token.nesting == -1 ? "</" : "<").append(token.tag);

        // Encode attributes, e.g. `<img src="foo"`
        result.append(this.renderAttrs(token));

        // Add a slash for self-closing tags, e.g. `<img src="foo" /`
        if (token.nesting == 0 && configuration.xhtmlOut)
        {
            result.append(" /");
        }

        // Check if we need to add a newline after this tag
        if (token.block)
        {
            needLf = true;

            if (token.nesting == 1)
            {
                if (idx + 1 < tokens.size())
                {
                    nextToken = tokens.get(idx + 1);

                    if ("inline".equals(nextToken.type) || nextToken.hidden)
                    {
                        // Block-level tag containing an inline tag.
                        needLf = false;
                    }
                    else if (nextToken.nesting == -1 && nextToken.tag.equals(token.tag))
                    {
                        // Opening tag + closing tag of the same type. E.g. `<li></li>`.
                        needLf = false;
                    }
                }
            }
        }
        result.append(needLf ? ">\n" : ">");

        return result.toString();
    }
    
    /**
     * Renderer.renderInline(tokens, options, env) -> String
     * - tokens (Array): list on block tokens to renter
     * - options (Object): params of parser instance
     * - env (Object): additional data from parsed input (references, for example)
     *
     * The same as [[Renderer.render]], but for single token of `inline` type.
     **/
    public String renderInline(List<Token> tokens, Configuration configuration, Environment env)
    {
        String type;
        String result = "";
        Map<String, IRender> rules = this.rules;

        for (int i = 0, len = tokens.size(); i < len; i++)
        {
            type = tokens.get(i).type;

            if (null != rules.get(type))
                result += rules.get(type).render(tokens, i, configuration, env, this);
            else
                result += this.renderToken(tokens, i, configuration);
        }

        return result;
    }


    /** internal
     * Renderer.renderInlineAsText(tokens, options, env) -> String
     * - tokens (Array): list on block tokens to renter
     * - options (Object): params of parser instance
     * - env (Object): additional data from parsed input (references, for example)
     *
     * Special kludge for image `alt` attributes to conform CommonMark spec.
     * Don't try to use it! Spec requires to show `alt` content with stripped markup,
     * instead of simple escaping.
     **/
    public String renderInlineAsText(List<Token> tokens, Configuration configuration, Environment env)
    {
        StringBuilder result = new StringBuilder();

        for (Token token : tokens)
        {
            if ("text".equals(token.type))
                result.append(token.content);
            else if ("image".equals(token.type))
                result.append(this.renderInlineAsText(token.children, configuration, env));
        }

        return result.toString();
    }
    
    /**
     * Renderer.render(tokens, options, env) -> String
     * - tokens (Array): list on block tokens to renter
     * - options (Object): params of parser instance
     * - env (Object): additional data from parsed input (references, for example)
     *
     * Takes token stream and generates HTML. Probably, you will never need to call
     * this method directly.
     **/
    public String render(List<Token> tokens, Configuration configuration, Environment env)
    {
        StringBuilder result = new StringBuilder();
        String type;
        
        Map<String, IRender> rules = this.rules;

        for (int i = 0, len = tokens.size(); i < len; i++)
        {
            type = tokens.get(i).type;
    
            if ("inline".equals(type))
                result.append(this.renderInline(tokens.get(i).children, configuration, env));
            else if (null != rules.get(type))
                result.append(rules.get(type).render(tokens, i, configuration, env, this));
            else
//                result.append(this.renderToken(tokens, i, configuration, env));
                // FIXME what's env
                result.append(this.renderToken(tokens, i, configuration));
        }
        return result.toString();
    }
}
